{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/ods_stickers.jpg\" />"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Открытый курс по машинному обучению. Сессия № 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Индивидуальный проект по анализу данных.\n",
    "# Предсказание наличия у пациента хронической почечной недостаточности\n",
    "**Автор:** Кудин Степан Сергеевич (kudin.stepan@yandex.ru)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "## Задача"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Постановка"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для индивидуального проекта был взят этот <a href=\"http://archive.ics.uci.edu/ml/datasets/Chronic_Kidney_Disease\" target=\"_blank\">датасет</a>. В нём содержатся различные данные о пациентах и информация о том, есть ли у них хроническая почечная недостаточность (ХПН). Задача состоит в том, чтобы на основе представленных данных в датасете научиться предсказывать, болен ли человек ХПН или нет.\n",
    "\n",
    "Причиной ХПН могут быть множество болезней, некоторые из них, если запустить процесс, приводят к полной деградации функции почек. Есть так же болезни, вызывающие ХПН, которые приводят к отказу почек **гарантировано**. Поэтому чем раньше и проще будет диагностироваться возникновение ХПН, тем больше людей будет спасено от терминальной стадии ХПН, ну или хотя бы функция их почек дойдёт до терминальной стадии за больший срок, что тоже не самый плохой результат."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "### Описание данных в датасете"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим, какие данные есть в датасете.\n",
    "\n",
    "Признаки:\n",
    "1. age (численный) -- возраст в годах;\n",
    "2. bp (численный) -- артериальное давление в мм/рт.ст.\n",
    "3. sg (категориальный) -- удельная плотность, судя по всему, мочи. Возможные значения -- 1.005, 1.010, 1.015, 1.020, 1.025.\n",
    "4. al (категориальный) -- альбумин. Возможные значения -- 0, 1, 2, 3, 4, 5.\n",
    "5. su (категориальный) -- сахар. Возможные значения -- 0, 1, 2, 3, 4, 5.\n",
    "6. rbc (категориальный) -- красные кровяные тельца. Возможные значения -- normal, abnormal.\n",
    "7. pc (категориальный) -- гнойные клетки. Возможные значения -- normal, abnormal.\n",
    "8. pcc (категориальный) -- комки гнойных клеток. Возможные значения -- present, notpresent.\n",
    "9. ba (категориальный) -- бактерии. Возможные значения -- present, notpresent.\n",
    "10. bgr (численный) -- случайный тест глюкозы. Этот тест измеряет концентрацию глюкозы в крови в любое время без каких-нибудь предварительных условий (на тощак и т.д.). Яркий пример -- приборы для измерения уровня глюкозы у диабетиков. Измеряется в мг/дл.\n",
    "11. bu (численный) -- мочевина в крови. Измеряется в мг/дл.\n",
    "12. sc (численный) -- креатенин в сыворотке. Изменяется в мг/дл.\n",
    "13. sod (численный) -- натрий. Измеряется в миллиэквивалентах/л.\n",
    "14. pot (численный) -- калий. Измеряется в миллиэквивалентах/л.\n",
    "15. hemo (численный) -- гемоглобин. В описании данных указано, что измеряется в gms, но судя по всему, тут опечатка и измеряется в г/мл (возможно, требуется изучить данные :-)).\n",
    "16. pcv (численный) -- гематокрит (объём упакованных клеток).\n",
    "17. wc (численный) -- количество белых кровяных клеток. Измеряется в количестве клеток на микролитр.\n",
    "18. rc (численный) -- количество эритроцитов. Измеряется в миллионах на кубический сантиметр.\n",
    "19. htn (категориальный) -- гипертония. Возможные значения -- yes, no.\n",
    "20. dm (категориальный) -- сахарный диабет. Возможные значения -- yes, no.\n",
    "21. cad (категориальный) -- коронарная недостаточность. Возможные значения -- yes, no.\n",
    "22. appet (категориальный) -- аппетит. Возможные значения -- good, poor.\n",
    "23. pe (категориальный) -- отёк ног. Возможные значения -- yes, no.\n",
    "24. ane (категориальный) -- анемия. Возможные значения -- yes, no.\n",
    "\n",
    "Целевой признак class является категориальным и имеет два значения ckd (есть ХПН) и notckd (нет ХПН)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "## Первичная подготовка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сначала загрузим датасет. Предварительно, я руками сконвертировал его в формат csv (scipy'евский ридер, исходный датасет не смог прочитать)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Константы.\n",
    "RANDOM_SEED = 17\n",
    "PATH_TO_DATASET_FILE = \"../../data/chronic_kidney_disease.csv\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "raw_dataset = pd.read_csv(PATH_TO_DATASET_FILE)\n",
    "\n",
    "raw_dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Размер таблицы:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "raw_dataset.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "и общая информация о датасете:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "raw_dataset.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Видно, что пандас при чтении посчитал все признаки объектами. Прежде чем начать анализировать датасет, преобразуем датасет в более удобоваримый формат:\n",
    "1. заменим строковые значения категориальных признаков на числовые (простое индексирование);\n",
    "2. заменим знаки вопроса, обозначающие пропуск значения на NaN;\n",
    "3. приведём все признаки к типу float64.\n",
    "\n",
    "Поехали:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "fixed_dataset = pd.DataFrame()\n",
    "passes_filler = lambda x: np.nan if str(x) == \"?\" else x\n",
    "\n",
    "numerical_features = [\"age\", \"bp\", \"bgr\", \"bu\", \"sc\", \"sod\", \"pot\", \"hemo\", \"pcv\", \"wc\", \"rc\"]\n",
    "nominal_features = [\"sg\", \"al\", \"su\", \"rbc\", \"pc\", \"pcc\", \"ba\", \"htn\", \"dm\", \"cad\", \"appet\", \"pe\", \"ane\", \"class\"]\n",
    "nominal_features_mapping = [\n",
    "    {\"1.005\": 0, \"1.010\": 1, \"1.015\": 2, \"1.020\": 3, \"1.025\": 4},\n",
    "    {\"0\": 0, \"1\": 1, \"2\": 2, \"3\": 3, \"4\": 4, \"5\": 5},\n",
    "    {\"0\": 0, \"1\": 1, \"2\": 2, \"3\": 3, \"4\": 4, \"5\": 5},\n",
    "    {\"normal\": 0, \"abnormal\": 1},\n",
    "    {\"normal\": 0, \"abnormal\": 1},\n",
    "    {\"present\": 0, \"notpresent\": 1},\n",
    "    {\"present\": 0, \"notpresent\": 1},\n",
    "    {\"yes\": 0, \"no\": 1},\n",
    "    {\"yes\": 0, \"no\": 1},\n",
    "    {\"yes\": 0, \"no\": 1},\n",
    "    {\"good\": 0, \"poor\": 1},\n",
    "    {\"yes\": 0, \"no\": 1},\n",
    "    {\"yes\": 0, \"no\": 1},\n",
    "    {\"ckd\": 1, \"notckd\": 0}\n",
    "]\n",
    "\n",
    "for feature in numerical_features:\n",
    "    fixed_dataset[feature] = raw_dataset[feature].map(passes_filler).astype(\"float64\")\n",
    "\n",
    "for num, feature in enumerate(nominal_features):\n",
    "    fixed_dataset[feature] = raw_dataset[feature].map(passes_filler).map(nominal_features_mapping[num])\n",
    "\n",
    "fixed_dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь в raw_dataset лежат данные в том виде, с которым можно работать."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Первичный анализ датасета"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Пришло время внимательно посмотреть на данные. Для начала посмотрим на отношения классов, для этого нарисуем гистограмму, заодно импортируем библиотеки для рисования и настроим их:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pylab as plt\n",
    "%matplotlib inline\n",
    "\n",
    "import seaborn as sns\n",
    "from matplotlib import pyplot as plt\n",
    "\n",
    "plt.rcParams[\"figure.figsize\"] = (10, 8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fixed_dataset[\"class\"].value_counts().plot(kind=\"bar\", label=\"ckd\")\n",
    "plt.legend()\n",
    "plt.title(\"Распределение признака ХПН в датасете\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что классы не сбалансированы, людей с ХПН в датасете больше, чем без него, но соотношение мощности классов меньше двух, иными словами данных о больных ХПН в датасете не подавляющее большинство."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим статистику по нашим числовым данным, попробуем понять, на сколько чистые данные у нас в наличии."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fixed_dataset.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В описании датасета написано, что данные реальные, правдоподобность большинства значений признаков, мне, как не специалисту оценить крайне затруднительно. В целом, я думаю, что надо оставить все данные, даже не смотря на то, что минимальный возраст пациента 2 года, увы, ХПН бывает и в таком возрасте. К тому же, экстремальные значения признака скорее всего будет говорить о наличии какой-либо паталогии."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Прежде чем пытаться дальше анализировать данные нужно обработать пропущенные значения в датасете. Поскольку, в теории, может быть пропущен любой признак, кроме целевого, то, будут обработаны как категориальные признаки, так и численные.\n",
    "\n",
    "Пропуски в численных признаках попробую заместить средним значением. Поэтому деление на тренировочную и тестовую выборку я выполню сейчас, чтобы не использовать данные из теста для подсчёта среднего по признаку. Разбиение будет стратифицированным, чтобы распределения целевого признака на тренировочном и тестовом датасете были одинаковы. На тестовую часть будет выделено 25% датасета."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_tmp = fixed_dataset[\"class\"].copy(deep=True)\n",
    "X_tmp = fixed_dataset.copy(deep=True).drop(\"class\", axis=1)\n",
    "\n",
    "from sklearn.model_selection import StratifiedShuffleSplit\n",
    "\n",
    "sss = StratifiedShuffleSplit(n_splits=1, test_size=0.25, random_state=RANDOM_SEED)\n",
    "train_index, test_index = tuple(sss.split(X_tmp, y_tmp))[0]\n",
    "\n",
    "train = fixed_dataset.iloc[train_index]\n",
    "test = fixed_dataset.iloc[test_index]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На всякий случай проверю, что распределения целевого признака на тестовой и тренировочной выборках примерно одинаковы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train[\"class\"].value_counts().plot(kind=\"bar\", label=\"ckd\")\n",
    "plt.legend()\n",
    "plt.title(\"Распределение ХПН в датасете train\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test[\"class\"].value_counts().plot(kind=\"bar\", label=\"ckd\")\n",
    "plt.legend()\n",
    "plt.title(\"Распределение ХПН в датасете test\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Действительно, то что надо."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь, заполним пропуски в данных. Для численных признаков я попробую использовать среднее. В категориальные признаки же введу ещё одно значение, обозначающее то, что признак отсутствует."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset = fixed_dataset.copy(deep=True)\n",
    "\n",
    "# Обработка численных признаков.\n",
    "numerical_features_mean = []\n",
    "for feature in numerical_features:\n",
    "    mean_val = train[feature].mean()\n",
    "    numerical_features_mean.append(mean_val)\n",
    "    train[feature].fillna(mean_val, inplace=True)\n",
    "    test[feature].fillna(mean_val, inplace=True)\n",
    "    dataset[feature].fillna(mean_val, inplace=True)\n",
    "    \n",
    "# Обработка категориальных признаков.\n",
    "# У этого списка признаков только два значения.\n",
    "cutted_nominal_features = [\"rbc\", \"pc\", \"pcc\", \"ba\", \"htn\", \"dm\", \"cad\", \"appet\", \"pe\", \"ane\"]\n",
    "for feature in cutted_nominal_features:\n",
    "    train[feature].fillna(2, inplace=True)\n",
    "    test[feature].fillna(2, inplace=True)\n",
    "    dataset[feature].fillna(2, inplace=True)\n",
    "\n",
    "# Оставшиеся обработаю отдельно.\n",
    "# \"sg\", \"al\", \"su\"\n",
    "train[\"sg\"].fillna(5, inplace=True)\n",
    "test[\"sg\"].fillna(5, inplace=True)\n",
    "dataset[\"sg\"].fillna(5, inplace=True)\n",
    "\n",
    "train[\"al\"].fillna(6, inplace=True)\n",
    "test[\"al\"].fillna(6, inplace=True)\n",
    "dataset[\"al\"].fillna(6, inplace=True)\n",
    "\n",
    "train[\"su\"].fillna(6, inplace=True)\n",
    "test[\"su\"].fillna(6, inplace=True)\n",
    "dataset[\"su\"].fillna(6, inplace=True)\n",
    "\n",
    "dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для удобства сконвертируем значения категориальных признаков в целые числа."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for feature in nominal_features:\n",
    "    train[feature] = train[feature].astype(\"int8\")\n",
    "    test[feature] = test[feature].astype(\"int8\")\n",
    "    dataset[feature] = dataset[feature].astype(\"int8\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отлично, теперь у нас есть датасет без пропусков. Давайте посмотрим на значения, которые принимают наши категориальные признаки в тренировочной части датасета."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_uniques = pd.melt(frame=train, value_vars=nominal_features)\n",
    "train_uniques = pd.DataFrame(train_uniques.groupby([\"variable\", \"value\"])[\"value\"].count()) \\\n",
    "    .sort_index(level=[0, 1]) \\\n",
    "    .rename(columns={\"value\": \"count\"}) \\\n",
    "    .reset_index()\n",
    "    \n",
    "sns.factorplot(x=\"variable\", y=\"count\", hue=\"value\", data=train_uniques, kind=\"bar\", size=12);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из графика видно, что почти у всех категориальных признаков есть доминирующее значение, а так же видно, что у некоторых признаков очень много пропусков, возможно, их не стоит учитывать при постоении модели. Давайте посмотрим на численную статистику по уникальным значениям у категориальных признаков:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for feature in nominal_features:\n",
    "    n = train[feature].nunique()\n",
    "    print(\"Feature: %s\" % feature)\n",
    "    print(n, sorted(train[feature].value_counts().to_dict().items()))\n",
    "    print(10 * \"-\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Эту статистику будем использовать в дальнейшем, если понадобится."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте разобьём категориальные элементы датасета по значениям целевого признака. Возможно, это покажет нам самые важные признаки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_uniques = pd.melt(frame=train, value_vars=nominal_features[:-1], id_vars=[\"class\"])\n",
    "train_uniques = pd.DataFrame(train_uniques.groupby([\"variable\", \"value\", \"class\"])[\"value\"].count()) \\\n",
    "    .sort_index(level=[0, 1]) \\\n",
    "    .rename(columns={\"value\": \"count\"}) \\\n",
    "    .reset_index()\n",
    "    \n",
    "sns.factorplot(x=\"variable\", y=\"count\", hue=\"value\", \n",
    "               col=\"class\", data=train_uniques, kind=\"bar\", size=9);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что при отсутствии ХПН при наличии значения альбумина (al) он принимает только нулевое значение. Так же, если нет ХПН, то и значение анемии (ane) нет. Только при наличии ХПН пропадает аппетит (appet). Подобное можно написать про **все** категориальные признаки, в каждом есть значения, которые встречаются в одном классе и почти или вообще не встречаются в другом классе.\n",
    "\n",
    "Это выглядит абсолютно логичным, можно предположить, что значения, которые в основном встречаются только при наличии ХПН являются не нормальными.\n",
    "\n",
    "Например, гипертония, у тех у кого нет ХПН её нет в тренировочной выборке. Это объясняется тем, одной из причин гипертонии (это значит, что у человека повышенное артериальное давление) с одной стороны могут являться болезни почек, из-за того, что скорость фильтрации почек падает (СКФ - скорость клубочковой фильтрации, клубочек - структурная единица почки, выполняющаяя фильтрационную функцию) и организм пытается компенсировать это повышением артериального давления, что скорость фильтрации увеличивает и ускоряет деградацию функции почки. С другой стороны, гипертония, вызванная другими причинами негативно влияет на почку, разрушая её структурные единицы.T ак же при ХПН может появляться отсутствие аппетита и так далее и тому подобное. В общем очень тяжёлое заболевание, поэтому то, что все категориальные признаки являются достаточно сильными по медицинским причинам."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь попробуем проанализировать численные признаки. Наприсуем boxplot'ы, описывающее статистики распределения количественных признаков в двух группах: у кого нет ХПН и у кого оно есть."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(nrows=4, ncols=3, figsize=(16, 10))\n",
    "\n",
    "for index, feature in  enumerate(numerical_features):\n",
    "    sns.boxplot(x=\"class\", y=feature, data=train, ax=axes[int(index / 3), index % 3])\n",
    "    axes[int(index / 3), index % 3].legend()\n",
    "    axes[int(index / 3), index % 3].set_xlabel(\"class\")\n",
    "    axes[int(index / 3), index % 3].set_ylabel(feature);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На глаз, наибольшие различия у признаков hemo (гемоглобин), pcv (гематокрит), rc (количество эритроцитов), age (возраст). Логично, при ХПН деградируют все функции почки, в том числе её функция при создании гемоглобина в крови. Гематокрит и количество эритроцитов тоже снижаются. Возраст важен потому, что болезнь чаще встречается у людей старше 30. Скорее всего это сильные признаки.\n",
    "\n",
    "Так же, хочу обратить внимание на признаки bu (мочевина) и sc (креатенит), не смотря на то что они довольно мелко отрисовались, особенно креатенин, если присмотреться дожно быть видно довольно сильное отличие при разных значениях класса, связано это с тем, что мочевина и креатенин являются азотистыми шлаками, вырабатывающимися при жизнидеятельности человека и их выводом занимаются почки. Чем большая степень ХПН, тем хуже они фильтруют и тем больше концентация азотистых шлаков в крови. Скорее всего, они тоже будут сильными признаками."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И последнее, изучим корреляцию между количественными признаками."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "corr_matrix = train.drop(nominal_features, axis=1).corr()\n",
    "sns.heatmap(corr_matrix, annot=True, fmt=\".2f\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Используя таблицу Чеддока, получаем, что между собой сильно коррелируют (модуль больше или равен 0.7) признаки pcv и hemo, rc и pcv, заметно коррелируют (модуль от 0.5 до 0.7 не включая) sc и bu, sod и sc, hemo и bu, pcv и bu, rc и hemo, умеренно (модуль от 0.3 до 0.5 не включая) sod и bu, pot и bu, hemo и sc, hemo и sod, pcv и sc, pcv и sod, rc и sc, rc и sod. Корреляции сильные и заметные корреляции объясняются медицинскими причинами.\n",
    "\n",
    "Используем эту информацию при создании модели."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Построение модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поскольку задача медицинская, крайне необходимо, чтобы алгоритм легко интерпретировался. Поэтому будет строиться дерево решений. В качестве метрики качества я выбираю F1 меру, поскольку, не смотря на то, что нужна как можно большая полнота (мы должны пропускать как можно меньше больных людей), нам нужен баланс с точностью, иначе константное решение будет оптимальным, но мы не хотим проводить дополнительные обследования для подтверждения наличия ХПН всем подряд (в первую очередь измерять СКФ)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сначала окончательно подготовим датасет для тренировки моделей. Сделаем dummy кодирование для категориальных признаков и выкинем из выборки по одному признаку из сильно коррелирующие пар. Нормировать данные не надо, поскольку это не нужно для применения алгоритма решающего дерева."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_datasets(df, train_index, test_index, numerical_features, nominal_features):\n",
    "    X_train = pd.DataFrame()\n",
    "    y_train = pd.DataFrame()\n",
    "\n",
    "    X_test = pd.DataFrame()\n",
    "    y_test = pd.DataFrame()\n",
    "\n",
    "    # Сначала добавим количественные признаки.\n",
    "    for feature in numerical_features:\n",
    "        X_train[feature] = train[feature]\n",
    "        X_test[feature] = test[feature]\n",
    "\n",
    "    # dummy кодирование категориальных признаков.\n",
    "    for feature in nominal_features:\n",
    "        X_train = pd.concat([X_train, pd.get_dummies(df[feature], feature).iloc[train_index]], axis=1)\n",
    "        X_test = pd.concat([X_test, pd.get_dummies(df[feature], feature).iloc[test_index]], axis=1)\n",
    "\n",
    "    y_train = df[\"class\"].iloc[train_index].copy(deep=True)\n",
    "    y_test = df[\"class\"].iloc[test_index].copy(deep=True)\n",
    "    \n",
    "    return X_train, X_test, y_train, y_test\n",
    "    \n",
    "\n",
    "clean_numerical_features = [\"age\", \"bp\", \"bgr\", \"bu\", \"sc\", \"sod\", \"pot\", \"hemo\", \"pcv\", \"wc\", \"rc\"]\n",
    "clean_nominal_features = [\"sg\", \"al\", \"su\", \"rbc\", \"pc\", \"pcc\", \"ba\", \"htn\", \"dm\", \"cad\", \"appet\", \"pe\", \"ane\"]\n",
    "\n",
    "X_train, X_test, y_train, y_test = get_datasets(dataset, train_index, test_index, clean_numerical_features,\n",
    "                                                clean_nominal_features)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "С помощью кросс валидации найдём оптимальную глубину дерева. Количество фолдов будет равно 3, поскольку данных очень мало."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.tree import DecisionTreeClassifier\n",
    "from sklearn.model_selection import GridSearchCV\n",
    "\n",
    "\n",
    "tree_params = {\"max_depth\": list(range(2, 11))}\n",
    "\n",
    "tree = DecisionTreeClassifier(random_state=RANDOM_SEED)\n",
    "gscv = GridSearchCV(tree, tree_params, scoring=\"f1\", cv=3, n_jobs=-1, verbose=True)\n",
    "gscv.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Какая оценка F1 score у нас получилась?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gscv.best_score_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Выглядит очень хорошо. Проверим результат на тестовой выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import f1_score, accuracy_score\n",
    "\n",
    "def evaluate_model(model, X_text, y_test):\n",
    "    y_test_pred = model.predict(X_test)\n",
    "\n",
    "    base_test_f1_score = f1_score(y_test, y_test_pred)\n",
    "    base_accuracy_score = accuracy_score(y_test, y_test_pred)\n",
    "\n",
    "    print(\"Test F1 score: %f\" % base_test_f1_score)\n",
    "    print(\"Test accuracy score: %f\" % base_accuracy_score)\n",
    "\n",
    "evaluate_model(gscv.best_estimator_, X_test, y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Результ просто отличный!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Надо понимать, что для того чтобы получить такой результат мы используем очень много признаков, а значит нужно много анализов и исследований. Давайте попробуем сократить количество признаков и не потерять существенно в качестве. Можно вспомнить, что у нас есть числовые признаки, на которых распределения значений сильно различаются на тренировочной и тестовой выборке, а так же то, что у нас есть категориальные признаки, в которых много пропусков. Давайте возьмём только самые существенные числовые признаки (учитывая корреляци, естественно) и те категориальные, в которых мало пропусков и обучим новую модель."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clean_numerical_features = [\"age\", \"bu\", \"sc\", \"hemo\", \"rc\"]\n",
    "clean_nominal_features = [\"pcc\", \"ba\", \"htn\", \"dm\", \"cad\", \"appet\", \"pe\", \"ane\"]\n",
    "\n",
    "X_train, X_test, y_train, y_test = get_datasets(dataset, train_index, test_index, clean_numerical_features,\n",
    "                                                clean_nominal_features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tree_params = {\"max_depth\": list(range(2, 11))}\n",
    "\n",
    "tree = DecisionTreeClassifier(random_state=RANDOM_SEED)\n",
    "gscv = GridSearchCV(tree, tree_params, scoring=\"f1\", cv=3, n_jobs=-1, verbose=True)\n",
    "gscv.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Результат модели:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gscv.best_score_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И результат на тестовом датасете:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "evaluate_model(gscv.best_estimator_, X_test, y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что результат стал чуть хуже, зато мы избавились от кучи параметров. Уменьшим количество признаков ещё сильнее. Используем численные признаки из предыдущей модели, поскольку если креатенин высокий, то и мочевина скорее всего тоже, за исключением мочевины, а из категориальных оставим только гипертонию и сахарный диабет, поскольку это группы риска для ХПН, и аппетит и отёки ног, поскольку легко диагностируются и при ХПН встречаются довольно часто."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clean_numerical_features = [\"age\", \"hemo\", \"rc\", \"sc\"]\n",
    "clean_nominal_features = [\"dm\", \"htn\", \"appet\", \"pe\"]\n",
    "\n",
    "X_train, X_test, y_train, y_test = get_datasets(dataset, train_index, test_index, clean_numerical_features,\n",
    "                                                clean_nominal_features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tree_params = {\"max_depth\": list(range(2, 11))}\n",
    "\n",
    "tree = DecisionTreeClassifier(random_state=RANDOM_SEED)\n",
    "gscv = GridSearchCV(tree, tree_params, scoring=\"f1\", cv=3, n_jobs=-1, verbose=True)\n",
    "gscv.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оценка качества модели на кросс-валидации:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gscv.best_score_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оценка качества на тестовой выборке:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "evaluate_model(gscv.best_estimator_, X_test, y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Качество осталось примерно таким же, при этом с очень высоким качеством мы определяем наличие ХПН в два анализа крови -- общий, чтобы получить гемоглобин и эритроциты, и биохимический -- для того чтобы получить креатенин, при учёте двух групп риска -- гипертоники и больные сахарным диабетом, а так же используя два крайне легко диагностируемых самостоятельно признака -- отсутствие аппетита и отёки ног."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отсутствие аппетита и отёк ног - одно из возможных следствий ХПН. Давайте, сделаем признак possible_consequence_of_disease, который будет иметь значение 1, если у нас есть хотя бы одино из этих следствий присутствует у пациета, 0, если отсутствуют оба.\n",
    "\n",
    "Так же, при ХПН креатенин всегда выше нормы, поэтому попробуем обойтись без общего анализа крови и возьмём только биохимию на креатенин у пациета.\n",
    "\n",
    "Также сделаем признак risk_group, его значение будет равно 1, если у пациента есть сахарный диабет и/или гипертония, и 0, если их нет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clean_numerical_features = [\"age\", \"sc\"]\n",
    "clean_nominal_features = []\n",
    "\n",
    "X_train, X_test, y_train, y_test = get_datasets(dataset, train_index, test_index, clean_numerical_features,\n",
    "                                                clean_nominal_features)\n",
    "\n",
    "tmp_1 = dataset[\"appet\"].map({0: 1, 1: 0, 2: 2})\n",
    "tmp_2 = dataset[\"pe\"].map({0: 1, 1: 0, 2: 2})\n",
    "tmp = pd.Series([tmp_1[index] or tmp_2[index] for index in range(tmp_1.shape[0])])\n",
    "features = pd.get_dummies(tmp, \"pcd\")\n",
    "\n",
    "X_train = pd.concat([X_train, features.iloc[train_index].copy(deep=True)], axis=1)\n",
    "X_test = pd.concat([X_test, features.iloc[test_index].copy(deep=True)], axis=1)\n",
    "\n",
    "\n",
    "tmp_1 = dataset[\"dm\"].map({0: 1, 1: 0, 2: 2})\n",
    "tmp_2 = dataset[\"htn\"].map({0: 1, 1: 0, 2: 2})\n",
    "tmp = pd.Series([tmp_1[index] or tmp_2[index] for index in range(tmp_1.shape[0])])\n",
    "features = pd.get_dummies(tmp, \"risk_group\")\n",
    "\n",
    "X_train = pd.concat([X_train, features.iloc[train_index].copy(deep=True)], axis=1)\n",
    "X_test = pd.concat([X_test, features.iloc[test_index].copy(deep=True)], axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tree_params = {\"max_depth\": list(range(2, 11))}\n",
    "\n",
    "tree = DecisionTreeClassifier(random_state=RANDOM_SEED)\n",
    "gscv = GridSearchCV(tree, tree_params, scoring=\"f1\", cv=3, n_jobs=-1, verbose=True)\n",
    "gscv.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оценка качества модели на кросс-валидации:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gscv.best_score_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оценка качества на тестовой выборке:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "evaluate_model(gscv.best_estimator_, X_test, y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Качество на тестовой выборке осталось таким же. В итоге мы получили очень простой классификатор, с очень хорошим качеством предсказания. По сути, для определения с очень высокой вероятностью есть ли у человека ХПН на нужно знать возраст человека, креатенин крови по биохимии, входит ли он в группу риска и есть ли у него возможные наблюдаемые следствия."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Достаточно ли данных и можно ли улучшить модель"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Интуитивно - у нас очень мало данных, всего о 400 пациентах, при этом 100 ушло в тестовую выборку. Как минимум для теста нужно сильно больше данных. Так же у нас есть заметный перекос в сторону больных ХПН людей, что вполне может сказываться на качестве модели, ведь в реальности, к счастью, ХПН встречается не часто, у большинства людей его нет."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте исследуем последнюю модель на предмет улучшения качества и построим кривые валидации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cv_results = pd.DataFrame(gscv.cv_results_)\n",
    "\n",
    "plt.plot(cv_results[\"param_max_depth\"], cv_results[\"mean_train_score\"], label=\"mean_train_score\")\n",
    "plt.plot(cv_results[\"param_max_depth\"], cv_results[\"mean_test_score\"], label=\"mean_test_score\")\n",
    "plt.legend(loc=\"best\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим, какая у нас глубина у результата:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gscv.best_params_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "По кривым валидации видно, что модель почти сразу началась переобучаться, в смысле получения наилучшего результата мы получили наилучший результат."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Сохраним наилучшую модель.\n",
    "best_model = gscv.best_estimator_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построим кривые обучения, чтобы понять, достаточно ли данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Проценты от обучающей выборки для разделения.\n",
    "split_percents = [index / 10 for index in range(1, 9)]\n",
    "test_scores = []\n",
    "train_scores = []\n",
    "\n",
    "for split_percent in split_percents:\n",
    "    sss = StratifiedShuffleSplit(n_splits=1, test_size=split_percent, random_state=RANDOM_SEED)\n",
    "    train_index, _ = tuple(sss.split(X_train, y_train))[0]\n",
    "    X_train_tmp = X_train.iloc[train_index].copy(deep=True)\n",
    "    y_train_tmp = y_train.iloc[train_index].copy(deep=True)\n",
    "    \n",
    "    tree_params = {\"max_depth\": list(range(2, 10))}\n",
    "\n",
    "    tree_2 = DecisionTreeClassifier(random_state=RANDOM_SEED)\n",
    "    gscv_2 = GridSearchCV(tree_2, tree_params, scoring=\"f1\", cv=3, n_jobs=-1, verbose=True)\n",
    "    gscv_2.fit(X_train_tmp, y_train_tmp)\n",
    "    \n",
    "    test_score = gscv_2.best_score_\n",
    "    train_score = f1_score(y_train_tmp, gscv_2.best_estimator_.predict(X_train_tmp))\n",
    "    \n",
    "    test_scores.append(test_score)\n",
    "    train_scores.append(train_score)  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.plot(split_percents, test_scores, label=\"test_score\")\n",
    "plt.plot(split_percents, train_scores, label=\"train_score\")\n",
    "plt.legend(loc=\"best\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Кривые к друг другу ещё не сошлись, качество на валидации растёт, соответственно добавление данных действительно должно дать результат."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Получился хороший прототип для простой диагностики хронической почечной недостаточности, во всяком случае на данном этапе задача выглядит решаемой. Для того, чтобы её действительно решить, нужно больше данных."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
